/**
 * 
 */
package xj.toolkit.fsm.tmpl.state;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import xj.toolkit.fsm.FSMContext;
import xj.toolkit.fsm.FiniteStateMachine;
import xj.toolkit.fsm.State;
import xj.toolkit.fsm.event.TimeoutEvent;


/**
 * @author isdom
 *
 */
public class StateAdapter implements State {

	public static String innerClassToName(Class<?> c) {
		return	null != c ? c.getSimpleName() : null;
	}
	
    private static final Logger logger = 
    	LoggerFactory.getLogger(StateAdapter.class);
    
	private	Object	target;
	private	Method	methodOfEnter = null;
	private	Method	methodOfLeave = null;
	private	Method	methodOfTimeout = null;
	private	Map<Class<?>, Method>	methodsOfAccepts
	= new HashMap<Class<?>, Method>();
	
	private	String	name;
	
	/**
	 * @return the name
	 */
	public String getName() {
		return name;
	}

	/**
	 * @param name the name to set
	 */
	public void setNameOfClass(Class<?> c) {
		this.name = innerClassToName(c);
	}

	/**
	 * @return the methodOfTimeout
	 */
	public Method getMethodOfTimeout() {
		return methodOfTimeout;
	}

	/**
	 * @param methodOfTimeout the methodOfTimeout to set
	 */
	public void setMethodOfTimeout(Method methodOfTimeout) {
		if ( null != this.methodOfTimeout ) {
			throw new RuntimeException("more than one timeout method.");
		}
		this.methodOfTimeout = methodOfTimeout;
		this.methodOfTimeout.setAccessible(true);
	}

	/**
	 * @return the methodOfEnter
	 */
	public Method getMethodOfEnter() {
		return methodOfEnter;
	}

	/**
	 * @param methodOfEnter the methodOfEnter to set
	 */
	public void setMethodOfEnter(Method methodOfEnter) {
		if ( null != this.methodOfEnter ) {
			throw new RuntimeException("more than one enter method.");
		}
		this.methodOfEnter = methodOfEnter;
		this.methodOfEnter.setAccessible(true);
	}

	/**
	 * @return the methodOfLeave
	 */
	public Method getMethodOfLeave() {
		return methodOfLeave;
	}

	/**
	 * @param methodOfLeave the methodOfLeave to set
	 */
	public void setMethodOfLeave(Method methodOfLeave) {
		if ( null != this.methodOfLeave ) {
			throw new RuntimeException("more than one leave method.");
		}
		this.methodOfLeave = methodOfLeave;
		this.methodOfLeave.setAccessible(true);
	}

	/**
	 * @return the target
	 */
	public Object getTarget() {
		return target;
	}

	/**
	 * @param target the target to set
	 */
	public void setTarget(Object target) {
		this.target = target;
	}

	public void addMethodOfAccept(Class<?> c, Method methodOfAccept) {
		methodOfAccept.setAccessible(true);
		if ( null != this.methodsOfAccepts.get(c) ) {
			throw new RuntimeException("more than one accept method for [" + c + "].");
		}
		this.methodsOfAccepts.put(c, methodOfAccept);
	}
	
	public Class<?>[] getAcceptedEventTypes() {
		return	this.methodsOfAccepts.keySet().toArray(new Class<?>[0]);
	}
	
	private Method getMethodOf(Class<?> c) {
		if ( null == c ) {
			return	null;
		}
		Method m = this.methodsOfAccepts.get(c);
		if ( null != m ) {
			return	m;
		}
		Class<?>[] intfs = c.getInterfaces();
		for ( Class<?> intf : intfs ) {
			m = this.methodsOfAccepts.get(intf);
			if ( null != m ) {
				return	m;
			}
		}
		return getMethodOf(c.getSuperclass());
	}
	
	private	String	onTimeout(FiniteStateMachine fsm, FSMContext ctx, TimeoutEvent event) 
		throws Exception {
		if ( ctx.isTimeoutActive( event) ) {
			ctx.clearTimer();
			if ( null != this.methodOfTimeout ) {
				Class<?> nextStateClass = (Class<?>)this.methodOfTimeout.invoke(target, fsm, ctx);
				return	innerClassToName(nextStateClass);
			}
			else {
				logger.warn("can not found method for timeout event, stop ctx");
				return	null;
			}
		}
		else {
			return	this.name;
		}
	}
	
	/* (non-Javadoc)
	 * @see com.skymobi.fsm.State#acceptEvent(com.skymobi.fsm.FiniteStateMachine, com.skymobi.fsm.FSMContext, java.lang.Object)
	 */
	public String acceptEvent(FiniteStateMachine fsm, FSMContext ctx,
			Object event) {
		try {
			if ( null != event ) {
				if ( event instanceof TimeoutEvent ) {
					return	onTimeout(fsm, ctx, (TimeoutEvent)event);
				}
				else {
					Class<?> c = event.getClass();
					Method m = getMethodOf(c);
					if ( null != m ) {
						ctx.cancelTimer();
						Class<?> nextStateClass = (Class<?>) m.invoke(target, fsm, ctx, event);
						return	innerClassToName(nextStateClass);
					}
					else {
						logger.warn("can not found accept method for type {}, just ignore", c);
					}
				}
			}
		}
		catch (Exception e) {
			logger.error("acceptEvent:", e);
			return null;
		}
		return null;
	}

	/* (non-Javadoc)
	 * @see com.skymobi.fsm.State#enter(com.skymobi.fsm.FiniteStateMachine, com.skymobi.fsm.FSMContext)
	 */
	public boolean enter(FiniteStateMachine fsm, FSMContext ctx) {
		try {
			if ( null != this.methodOfEnter ) {
				return	(Boolean)this.methodOfEnter.invoke(target, fsm, ctx);
			}
			return true;
		}
		catch (Exception e) {
			logger.error("enter:", e);
			return	false;
		}
	}

	/* (non-Javadoc)
	 * @see com.skymobi.fsm.State#leave(com.skymobi.fsm.FiniteStateMachine, com.skymobi.fsm.FSMContext)
	 */
	public void leave(FiniteStateMachine fsm, FSMContext ctx) {
		try {
			if ( null != this.methodOfLeave ) {
				this.methodOfLeave.invoke(target, fsm, ctx);
				return;
			}
		}
		catch (Exception e) {
			logger.error("leave:", e);
		}
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return name;
	}
}
